// Copyright (C) 2006 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.ical.iter;

import com.google.ical.values.DateValue;
import java.util.Collection;
import java.util.Comparator;
import java.util.NoSuchElementException;
import java.util.PriorityQueue;

/**
 * a recurrence iterator that combines others.  Some may be inclusions, and
 * some may be exclusions.
 *
 * @author mikesamuel+svn@gmail.com (Mike Samuel)
 */
final class CompoundIteratorImpl implements RecurrenceIterator {

  /** a queue that keeps the earliest dates at the head */
  private PriorityQueue<HeapElement> queue;
  private HeapElement pending;
  /**
   * the number of inclusions on queue.  We keep track of this so that we
   * don't have to drain the exclusions to conclude that the series is
   * exhausted.
   */
  private int nInclusionsRemaining;

  /**
   * A generator that will generate only dates that are generated by inclusions
   * and will not generate any dates that are generated by exclusions -- i.e.
   * exclusions trump inclusions.
   * @param inclusions iterators whose elements should be included unless
   *   explicitly excluded.  non null without null elements.
   * @param exclusions iterators whose elements should not be included.
   *   non null without null elements.
   */
  CompoundIteratorImpl(
      Collection<RecurrenceIterator> inclusions,
      Collection<RecurrenceIterator> exclusions) {
    queue = new PriorityQueue<HeapElement>(
        inclusions.size() + exclusions.size(), HeapElement.CMP);
    for (RecurrenceIterator it : inclusions) {
      HeapElement el = new HeapElement(true, it);
      if (el.shift()) {
        queue.add(el);
        ++nInclusionsRemaining;
      }
    }
    for (RecurrenceIterator it : exclusions) {
      HeapElement el = new HeapElement(false, it);
      if (el.shift()) { queue.add(el); }
    }
  }

  public boolean hasNext() {
    requirePending();
    return null != pending;
  }

  public DateValue next() {
    requirePending();
    if (null == pending) { throw new NoSuchElementException(); }
    DateValue head = pending.head();
    reattach(pending);
    pending = null;
    return head;
  }

  public void remove() { throw new UnsupportedOperationException(); }

  public void advanceTo(DateValue newStart) {
    long newStartCmp = DateValueComparison.comparable(newStart);
    if (null != pending) {
      if (pending.comparable() >= newStartCmp) { return; }
      pending.advanceTo(newStart);
      reattach(pending);
      pending = null;
    }

    // Pull each element off the stack in turn, and advance it.
    // Once we reach one we don't need to advance, we're done
    while (0 != nInclusionsRemaining && !queue.isEmpty()
           && queue.peek().comparable() < newStartCmp) {
      HeapElement el = queue.poll();
      el.advanceTo(newStart);
      reattach(el);
    }
  }

  /**
   * if the given element's iterator has more data, then push back onto the
   * heap.
   */
  private void reattach(HeapElement el) {
    if (el.shift()) {
      queue.add(el);
    } else if (el.inclusion) {
      // if we have no live inclusions, then the rest are exclusions which we
      // can safely discard.
      if (0 == --nInclusionsRemaining) {
        queue.clear();
      }
    }
  }

  /**
   * make sure that pending contains the next inclusive HeapElement that doesn't
   * match any exclusion, and remove any dupes of it.
   */
  private void requirePending() {
    if (null != pending) { return; }

    long exclusionComparable = Long.MIN_VALUE;
    while (0 != nInclusionsRemaining && !queue.isEmpty()) {
      // find a candidate that is not excluded
      HeapElement inclusion = null;
      do {
        HeapElement candidate = queue.poll();
        if (candidate.inclusion) {
          if (exclusionComparable != candidate.comparable()) {
            inclusion = candidate;
            break;
          }
        } else {
          exclusionComparable = candidate.comparable();
        }
        reattach(candidate);
        if (0 == nInclusionsRemaining) { return; }
      } while (!queue.isEmpty());
      long inclusionComparable = inclusion.comparable();

      // Check for any following exclusions and for duplicates.
      // We could change the sort order so that exclusions always preceded
      // inclusions, but that would be less efficient and would make the
      // ordering different than the comparable value.
      boolean excluded = exclusionComparable == inclusionComparable;
      while (!queue.isEmpty()
             && queue.peek().comparable() == inclusionComparable) {
        HeapElement match = queue.poll();
        excluded |= !match.inclusion;
        reattach(match);
        if (0 == nInclusionsRemaining) { return; }
      }
      if (!excluded) {
        pending = inclusion;
        return;
      } else {
        reattach(inclusion);
      }
    }
  }

}

final class HeapElement {
  /**
   * should iterators items be included in the series or should they
   * nullify any matched items included by other series.
   */
  final boolean inclusion;
  /** the {@link DateValueComparison#comparable} for {@link #head}. */
  private long comparable;
  /** the last value removed from it.  In utc. */
  private DateValue head;
  private RecurrenceIterator it;

  HeapElement(boolean inclusion, RecurrenceIterator it) {
    this.inclusion = inclusion;
    this.it = it;
  }

  /** the last value removed from the iterator. */
  DateValue head() { return head; }
  /**
   * A given HeapElement may be compared to many others as it bubbles towards
   * the heap's root, so we cache this for each HeapElement.
   */
  long comparable() { return comparable; }
  /**
   * discard the current, and return true iff there is another head to
   * replace it.
   */
  boolean shift() {
    if (!it.hasNext()) { return false; }
    head = it.next();
    comparable = DateValueComparison.comparable(head);
    return true;
  }

  /**
   * advance the underlying iterator to the given date value a la
   * {@link RecurrenceIterator#advanceTo}.
   */
  void advanceTo(DateValue dvUtc) {
    it.advanceTo(dvUtc);
  }

  @Override
  public String toString() {
    return
      "[" + head.toString() + (inclusion ? ", inclusion]" : ", exclusion]");
  }

  /** compares to heap elements by comparing their heads. */
  static Comparator<HeapElement> CMP = new Comparator<HeapElement>() {
    public int compare(HeapElement a, HeapElement b) {
      long ac = a.comparable(),
           bc = b.comparable();
      return ac < bc ? -1 : ac == bc ? 0 : 1;
    }
  };

}
